package org.axed.user.client;

import java.util.Stack;
import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.Element;

/**
 * Manages all Undos/Redos. 
 */
public class UndoManager implements AxedListener {
	public static final int MAX_UNDO = 25;

	Stack undoStack = new Stack(); // TODO private
	Stack redoStack = new Stack();
	UndoBufferPool pool = new UndoBufferPool();

	/**
	 * Shortcut to the lastbuffer on the stack
	 */
	UndoBuffer lastBuffer = null;

	/**
	 * The AxedArea this belongs to.
	 */
	AxedArea axed;

	/**
	 * Constructor
	 */
	public UndoManager(AxedArea axed) {
		this.axed = axed;
	}

	/**
	 * Retrieves the undo stack.
	 */
	public Stack getUndoStack() {
		return undoStack;
	}

	/**
	 * Retrieves the redo stack.
	 */
	public Stack getRedoStack() {
		return redoStack;
	}

	/**
	 * TODO
	 */
	public void onAxedEvent(AxedEvent event) {
		if (event.atomicID < 0) {
			// this is already an undo in progress, don't put the event on the stack!
			return;
		}
		UndoBuffer ub;

		// clear redo buffer, if this isn't an undo/redo event, but any document changing
		// operation.
		switch (event.type) {
		case AxedEvent.REMOVE_CHAR :
		case AxedEvent.CLEARUNDOREDOSTACK :
		case AxedEvent.INSERT_CHAR :
		case AxedEvent.SPLIT_LINE :
		case AxedEvent.JOIN_LINE :
		case AxedEvent.INSERT_TEXT :
		case AxedEvent.REMOVE_TEXT :
			while (redoStack.size() > 0) {
				pool.releaseBuffer((UndoBuffer) redoStack.pop());
			}
		}

		switch (event.type) {
		case AxedEvent.REMOVE_CHAR :
			{
				AxedEvent.PntChar ev = event.getRemoveChar();
				switch (ev.tag) {
				case InputCatcher.KEY_BACKSPACE:
				case InputCatcher.KEY_DEL:
					removeChar(event.atomicID, ev);
					return;
				}
			}
			return;
		case AxedEvent.CLEARUNDOREDOSTACK :
			while (undoStack.size() > 0) {
				pool.releaseBuffer((UndoBuffer) undoStack.pop());
			}
			lastBuffer = null;
			return;
		case AxedEvent.MOVE_CURSOR:
			if (event.getMoveCursor().movetype != AxedEvent.MOVETYPE_FOLLOWUP) {
				lastBuffer = null;
			}
			return;
		case AxedEvent.INSERT_CHAR :
			insertChar(event.atomicID, event.getInsertChar());
			return;
		case AxedEvent.SPLIT_LINE :
			ub = newUndoBuffer(axed);
			ub.setSplitline(event.getSplitLine().pnt);
			ub.atomicID = event.atomicID;
			return;
		case AxedEvent.JOIN_LINE :
			ub = newUndoBuffer(axed);
			ub.setJoinLine(event.getJoinLine().pnt);
			ub.atomicID = event.atomicID;
			return;
		case AxedEvent.INSERT_TEXT :
			ub = newUndoBuffer(axed);
			ub.setInsText(event.getInsertText().iv, event.getInsertText().text);
			ub.atomicID = event.atomicID;
			return;
		case AxedEvent.REMOVE_TEXT :
			ub = newUndoBuffer(axed);
			ub.setDelText(event.getRemoveText().iv ,event.getRemoveText().text);
			ub.atomicID = event.atomicID;
			return;
		case AxedEvent.REDO :
			redo();
			return;
		case AxedEvent.UNDO :
			undo();
			return;
		}
	}

	/**
	 * A char has been backspaced.
	 */
	private void removeChar(int atomicID, AxedEvent.PntChar ev) {
		if (ev.ch == 0) {
			// this was a line remove, or didn't do anything.
			return;
		}
		UndoBuffer lb = lastBuffer;
		if (lb == null || lb.type != UndoBuffer.REMOVECHARS || lb.atomicID != atomicID) { 
			UndoBuffer ub = newUndoBuffer(axed);
			ub.setRemoveChars(ev.pnt);
			ub.append(ev.ch);
			ub.atomicID = atomicID;
			return;
		}
		if (ev.pnt.line == lb.p1.line && ev.pnt.col == lb.p1.col) {
			// append after
			lb.append(ev.ch);
			return;
		}
		if (ev.pnt.line == lb.p1.line && ev.pnt.col == lb.p1.col - 1) {
			// append before
			lb.insert(ev.ch, 0);
			lb.p1.col--;
			return;
		}
		UndoBuffer ub = newUndoBuffer(axed);
		ub.setRemoveChars(ev.pnt);
		ub.append(ev.ch);
		ub.atomicID = atomicID;
	}

	/**
	 * A char has been inserted.
	 */
	private void insertChar(int atomicID, AxedEvent.PntChar ev) {
		// when the last buff is the same type and same atomicID combine it
		// to save space/time, otherwise create a new undobuffer.
		UndoBuffer lb = lastBuffer;
		if (lb == null || lb.type != UndoBuffer.TEXT || lb.atomicID != atomicID ||
			ev.pnt.line != lb.p1.line || ev.pnt.col != lb.p1.col + lb.text.length()
		   ) { 
	   		UndoBuffer ub = newUndoBuffer(axed);
			ub.setText(ev.pnt);
			ub.append(ev.ch);
			ub.atomicID = atomicID;
			return;
		}
		lb.append(ev.ch);
	}

	/**
	 * Creates and pushes a new undo buffer.
	 */
	private UndoBuffer newUndoBuffer(AxedArea axed) {
		UndoBuffer ub = pool.newBuffer(axed);
		lastBuffer = ub;
		undoStack.push(ub);
		int us = undoStack.size();
		if (us > MAX_UNDO) {
			pool.releaseBuffer((UndoBuffer) undoStack.get(0));
			undoStack.remove(0);
		}
		return ub;
	}

	/**
	 * Does the undo.
	 */
	private void undo() {
		if (undoStack.size() == 0) {
			return;
		}
		UndoBuffer ub;
		lastBuffer = null;
		do { 										// undoes all buffers of the same atomicID.
		    ub = (UndoBuffer) undoStack.pop();
			switch (ub.type) {
			case UndoBuffer.REMOVECHARS :
				axed.insertText(AxedEvent.ATOMIC_UNDOREDO, ub.p1, ub.getText(), null);
				//axed.moveCursor(this, ub.linenum, ub.col + ub.getText().length());
				break;
			case UndoBuffer.DELTEXT :
				axed.insertText(AxedEvent.ATOMIC_UNDOREDO, ub.p1, ub.getText(), null);
				//axed.moveCursor(this, ub.linenum, ub.col);
				break;
			case UndoBuffer.INSTEXT :
				axed.removeText(AxedEvent.ATOMIC_UNDOREDO, ub.p1, ub.p2);
				//axed.moveCursor(this, ub.linenum, ub.col);
				break;
			case UndoBuffer.JOINLINE :
				axed.splitLine(AxedEvent.ATOMIC_UNDOREDO, ub.p1);
				//axed.moveCursor(this, ub.linenum + 1, 0);
				break;
			case UndoBuffer.SPLITLINE :
				axed.joinLine(AxedEvent.ATOMIC_UNDOREDO, ub.p1.line);
				break;
			case UndoBuffer.TEXT :
				axed.removeText(AxedEvent.ATOMIC_UNDOREDO, ub.p1.line, ub.p1.col, ub.p1.col + ub.getText().length()); 
				//axed.moveCursor(this, ub.linenum, bc);
				break;
			default:
				 throw new AxedInternalError("Wanting to undo unkown buffer.");
			}
			redoStack.push(ub);
		} while (ub.atomicID > 0 && undoStack.size() > 0 && ub.atomicID == ((UndoBuffer) undoStack.peek()).atomicID);
	}

	/**
	 * Does the redo.
	 */
	private void redo() {
		if (redoStack.size() == 0) {
			return;
		}
		UndoBuffer ub;
		lastBuffer = null;
		do { 										// redoes all buffers of the same atomicID.
			ub = (UndoBuffer) redoStack.pop();
			switch (ub.type) {
			case UndoBuffer.REMOVECHARS :
				axed.removeText(AxedEvent.ATOMIC_UNDOREDO, ub.p1, ub.p2);
				//TODO axed.moveCursor(this, ub.linenum, ub.col);
				break;
			case UndoBuffer.DELTEXT :
				axed.removeText(AxedEvent.ATOMIC_UNDOREDO, ub.p1, ub.p2);
				//axed.moveCursor(this, ub.linenum, ub.col);
				break;
			case UndoBuffer.INSTEXT :
				axed.insertText(AxedEvent.ATOMIC_UNDOREDO, ub.p1, ub.getText(), null);
				//axed.moveCursor(this, ub.linenum, ub.col);
				break;
			case UndoBuffer.JOINLINE :
				axed.joinLine(AxedEvent.ATOMIC_UNDOREDO, ub.p1.line);
				break;
			case UndoBuffer.SPLITLINE :
				axed.splitLine(AxedEvent.ATOMIC_UNDOREDO, ub.p1);
				//axed.moveCursor(this, ub.linenum + 1, 0);
				break;
			case UndoBuffer.TEXT :
				axed.insertText(AxedEvent.ATOMIC_UNDOREDO, ub.p1, ub.getText(), null);
				//axed.moveCursor(this, ub.linenum, bc + ub.getText().length());
				break;
			default:
				 throw new AxedInternalError("Wanting to redo unkown buffer.");		
			}
			undoStack.push(ub);
		} while (ub.atomicID > 0 && redoStack.size() > 0 && ub.atomicID == ((UndoBuffer) redoStack.peek()).atomicID);
	}
}
